選択範囲をMarkdown記法に変換してclip boardにcopyするPopupMenu
2020-10-16 08:57:37 できた
アイコン記法は無視している
動機
/icons/Scrapbox.iconに書いた文章をレポートに書き写すことがよくある
それを簡単にできるようにしたい
Wordの共同編集は重いので、3 ~ 4人で共同編集しようとすると固まったりエラーが出たり場合によってはデータが飛ぶことがある 代わりにreal time編集に優れた/icons/Scrapbox.iconで文章を書き、それをwordなりmarkdownなりに変換&提出すればレポート作成がとても楽になる 実装
見出し
トップレベルのみ
見出し以外は無視
アイコン記法は全て無視する
/icons/doing.iconコードブロックの言語名
言語名と拡張子との対応リストを別に作っておいたほうがいいな
switch文中に書いたりすると、読みにくくなる
こんな感じ
code:data.json
[
{
"filetype": "javascript"
},
]
2020-11-05 10:00:11 少しだけ実装した
やりたい
/icons/doing.icon相対的にindentを変える
例えば選択範囲の開始がインデントを1つ含んでいた場合は、それ以降1 indentをtop indentとして扱う
より少ないインデントの行を選択範囲中に含んでいた場合は、そちらを基準にする
それをこっちにscriptに移植すればいいだろう。
2020-11-12 01:45:28 codeだけ書いた
テストはしていない
refactoring
TypeScriptにする
arrow functionにする
テストを書く
/icons/hr.icon
似たようなUserScriptは既にある
よさそうtakker.icon
takker.iconは選択範囲に適用できるようにしたい
2023-02-06
21:47:07 iconをfont awesomeにした
2022-09-25
直した
2022-05-20
/icons/知らんかった.icon
試した
外部リンク記法は問題なく変換できる
https://xxx.comを変換すると[](https://xxx.com)にある
あーそういうことかtakker.icon
https://xxx.comのときは生のURLを直接貼ることにしよう
修正done
2022-05-15
その他、deno fmtで整形した
コードが雑な点には手を入れていない
そのうち直したいっちゃ直したい
code:script.js
import { convertSb2Md } from "./convert.js";
import { ScrapboxParser } from "../scrapbox-parser.min.js/parser.js";
// markdown変換
scrapbox.PopupMenu.addButton({
title: "\uf60f",
onClick: (text) => {
(async () => {
try {
const blocks = ScrapboxParser.parse(text, { hasTitle: false });
//console.log('Parserd text:');
//console.log(blocks);
// このindent levelを基準にする
const topIndentLevel = Math.min(...blocks.map((block) => block.indent));
await navigator.clipboard.writeText(
blocks.map((block) => convertSb2Md(block, topIndentLevel)).join("\n"),
);
//console.log('Copied.');
} catch (e) {
alert(Failed to copy:\n${JSON.stringify(e)});
}
})();
},
});
あとはblocksをmarkdownに変換するコードを書く必要がある
斜体
code:convert.js
// @ts-check
/**
*/
/** Scrapbox記法をMarkdown記法に変える
*
* @param {Block} block
* @param {number} topIndentLevel
* @return {string}
*/
export const convertSb2Md = (block, topIndentLevel) => {
switch (block.type) {
case "title":
return ""; // タイトルは選択範囲に入らないので無視
case "codeBlock":
return [
block.fileName,
\n\`\`\`${getFileType(block.fileName)},
block.content,
"\\`\\n",
].join("\n");
case "table":
return convertTable(block);
case "line":
return convertLine(block, topIndentLevel);
}
};
/** Table記法の変換
*
* @param {Table} table
* @return {string}
*/
const convertTable = (table) => {
// columnsの最大長を計算する
const maxCol = Math.max(...table.cells.map((row) => row.length));
table.cells.forEach((row, i) => {
line.push(
`| ${
row.map((column) => column.map((node) => convertNode(node)).join(""))
.join(" | ")
} |`,
);
if (i === 0) line.push(|${" -- |".repeat(maxCol)});
});
return line.join("\n");
};
const INDENT = " "; // インデントに使う文字
/** 行の変換
*
* @param {Line} line
* @param {number} topIndentLevel
* @return {string}
*/
const convertLine = (line, topIndentLevel) => {
const content = line.nodes
.map((node) =>
convertNode(node, { section: line.indent === topIndentLevel })
).join("").trim();
if (content === "") return ""; // 空行はそのまま返す
// リストを作る
if (line.indent === topIndentLevel) return content; // トップレベルの行はインデントにしない
let result = INDENT.repeat(line.indent - topIndentLevel - 1);
if (!/^\d+\. /.test(content)) result += "- "; // 番号なしの行は-を入れる
return result + content;
};
/** Nodeを変換する
*
* @param {NodeType} node
* @param {{section?:boolean}} init * @return {string}
*/
const convertNode = (node, init) => {
const { section = false } = init ?? {};
switch (node.type) {
case "quote":
return > ${node.nodes.map((node) => convertNode(node)).join("")};
case "helpfeel":
return \`? ${node.text}\`;
case "image":
case "strongImage":
return ![image](${node.src});
case "icon":
case "strongIcon":
// 仕切り線だけ変換する
.includes(node.path)
? "---"
: "";
case "strong":
return **${node.nodes.map((node) => convertNode(node)).join("")}**;
case "formula":
return $${node.formula}$;
case "decoration": {
let result = node.nodes.map((node) => convertNode(node)).join("");
if (node.decos.includes("/")) result = *${result}*;
// 見出しの変換
// お好みで変えて下さい
if (section) {
if (node.decos.includes("*-3")) result = # ${result}\n;
if (node.decos.includes("*-2")) result = ## ${result}\n;
if (node.decos.includes("*-1")) result = ### ${result}\n;
} else {
if (node.decos.some((deco) => /\*-/.test(deco0))) { result = **${result}**;
}
}
if (node.decos.includes("~")) result = ~~${result}~~;
return result;
}
case "code":
return \`${node.text}\`;
case "commandLine":
return \`${node.symbol} ${node.text}\`;
case "link":
switch (node.pathType) {
case "root":
return [${node.href}](https://scrapbox.io${node.href});
case "relative":
//@ts-ignore declare宣言が使えないため、scrapboxに型定義をつけられない
return [${node.href}](https://scrapbox.io/${scrapbox.Project.name}/${node.href});
default:
return node.content === "" ?
${node.href} :
[${node.content}](${node.href});
}
case "googleMap":
return [${node.place}](${node.url});
case "hashTag":
//@ts-ignore declare宣言が使えないため、scrapboxに型定義をつけられない
return [#${node.href}](https://scrapbox.io/${scrapbox.Project.name}/${node.href});
case "numberList":
return ${node.number}. ${node.nodes.map((node) => convertNode(node)).join("")};
case "blank":
case "plain":
return node.text;
}
};
あと$ \TeX用のも作りたい
コードブロックの言語識別用data
code:convert.js
const extensionData = [
{
fileType: "javascript",
},
{
fileType: "typescript",
},
{
fileType: "cpp",
},
{
fileType: "c",
},
{
fileType: "cs",
},
{
fileType: "markdown",
},
{
fileType: "html",
},
{
fileType: "json",
},
{
fileType: "xml",
},
{
fileType: "yaml",
},
{
fileType: "toml",
},
{
fileType: "ini",
},
{
fileType: "tex",
},
{
fileType: "svg",
},
];
コードブロックのファイル名から、programming言語を識別して返す
code:convert.js
/** ファイル名の拡張子から言語を取得する
*
* @param {string} filename
* @return {string}
*/
const getFileType = (filename) => {
const filenameExtention = filename.replace(/^.*\.(\w+)$/, "$1");
return extensionData
.find((data) => data.extensions.includes(filenameExtention))?.fileType ??
"";
};
/icons/hr.icon
前にも同じこと書いていたみたい
実装
1行ごとに変換
正規表現で構文解析する
indentの調節の仕方でoptionを作る
1. indentを維持
2. 深さに合わせてindentを相対的に減らす